/*
Copyright (c) 2008-2009, Oleg Estekhin
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name of the copyright holders nor the names of its
      contributors may be used to endorse or promote products derived from this
      software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package fractal.tile;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import fractal.math.fractal.Fractal;
import oe.math.complex.Complex;

/** todo write javadoc for TilePane. */
public final class TilePane extends JComponent {

    private static final int TILE_SIZE = 128;


    private Fractal fractal;

    private long scaledCenterX;

    private long scaledCenterY;

    private double scale;


    private TileImageCache tileImages;

    private final TileRenderer tileRenderer;


    /**
     * Creates a {@code TilePane} that initially will display the specified fractal.
     *
     * @param fractal the fractal
     *
     * @throws NullPointerException if {@code fractal} is {@code null}
     */
    public TilePane(Fractal fractal, TileRenderer tileRenderer) {
        setPreferredSize(new Dimension(800, 600));
        this.tileRenderer = tileRenderer;
        if (fractal == null) {
            throw new NullPointerException("fractal is null");
        }
        this.fractal = fractal;
        scaledCenterX = -128L;
        scaledCenterY = 0L;
        scale = 1.0/(4.0/800.0);

        tileImages = new TileImageCache(2048, 1024);
        addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                tileImages.setMaximumCacheSize(2 * (2 + getWidth() / TILE_SIZE) * (2 + getHeight() / TILE_SIZE));
            }
        });

        tileRenderer.setListener(new TileHandler());

        MouseAdapter mouseHandler = new MouseHandler();
        addMouseListener(mouseHandler);
        addMouseMotionListener(mouseHandler);
        addMouseWheelListener(mouseHandler);
    }


    /**
     * Returns the coordinates of the center.
     *
     * @return the coordinates of the center
     */
    public Complex getCenter() {
        return Complex.cartesian(scaledCenterX / scale, scaledCenterY / scale);
    }

    /**
     * Returns the scale.
     *
     * @return the scale
     */
    public double getScale() {
        return scale;
    }

    /**
     * Returns the fractal displayed by this pane.
     *
     * @return the fractal
     */
    public Fractal getFractal() {
        return fractal;
    }

    /**
     * Changes the fractal displayed by this pane.
     *
     * @param fractal the new fractal
     *
     * @throws NullPointerException if {@code fractal} is {@code null}
     */
    public void setFractal(Fractal fractal) {
        if (fractal == null) {
            throw new NullPointerException("fractal is null");
        }
        if (!fractal.equals(this.fractal)) {
            this.fractal = fractal;
            scaledCenterX = 0L;
            scaledCenterY = 0L;
            scale = 1.0;
            firePropertyChange("fractal", null, fractal); // todo
            repaint();
        }
    }


    @Override
    protected void paintComponent(Graphics graphics) {
        int width = getWidth();
        int height = getHeight();
        Rectangle clip = graphics.getClipBounds();

        long dataY = scaledCenterY + clip.y - height / 2;
        long shiftY = dataY % TILE_SIZE;
        long tileY = dataY - shiftY;
        if (shiftY < 0L) {
            tileY -= TILE_SIZE;
        }
        int drawY = clip.y + (int) (tileY - dataY);

        long dataX = scaledCenterX + clip.x - width / 2;
        long shiftX = dataX % TILE_SIZE;
        long tileXBase = dataX - shiftX;
        if (shiftX < 0L) {
            tileXBase -= TILE_SIZE;
        }
        int drawXBase = clip.x + (int) (tileXBase - dataX);

        while (drawY < clip.y + clip.height) {
            long tileX = tileXBase;
            int drawX = drawXBase;
            while (drawX < clip.x + clip.width) {
                graphics.drawImage(getTileImage(new Tile(tileX, tileY, scale, TILE_SIZE, TILE_SIZE, fractal)), drawX, drawY, null);
                tileX += TILE_SIZE;
                drawX += TILE_SIZE;
            }
            tileY += TILE_SIZE;
            drawY += TILE_SIZE;
        }
    }


    private Image getTileImage(Tile tile) {
        Image tileImage = tileImages.get(tile);
        if (tileImage == null) {
            // todo ask renderer
            // todo recreate if render does not have an image too
            tileImage = new BufferedImage(tile.getWidth(), tile.getHeight(), BufferedImage.TYPE_INT_ARGB);
            Graphics graphics = tileImage.getGraphics();
            graphics.setColor(getBackground());
            graphics.fillRect(0, 0, tile.getWidth(), tile.getHeight());
            graphics.setColor(getBackground().darker());
            graphics.drawRect(0, 0, tile.getWidth() - 1, tile.getHeight() - 1);
            graphics.setColor(getForeground());
            int fontHeight = graphics.getFontMetrics().getHeight();
            graphics.drawString("x=" + tile.getX(), 2, fontHeight);
            graphics.drawString("y=" + tile.getY(), 2, fontHeight * 2);
            graphics.drawString("s=" + tile.getScale(), 2, fontHeight * 3);
            graphics.dispose();
            tileImages.put(tile, tileImage);
            // todo request rendering
            tileRenderer.render(tile);
            return tileImage;
        } else {
            return tileImage;
        }
    }


    private static final class TileImageCache extends LinkedHashMap<Tile, Image> {

        private final int minimumCacheSize;

        private int maximumCacheSize;

        private TileImageCache(int initialCapacity, int minimumCacheSize) {
            super(initialCapacity, 0.75f, true);
            if (minimumCacheSize < 0) {
                throw new IllegalArgumentException("minimumCacheSize is negative");
            }
            this.minimumCacheSize = minimumCacheSize;
            maximumCacheSize = minimumCacheSize;
        }

        public int getMinimumCacheSize() {
            return minimumCacheSize;
        }

        public int getMaximumCacheSize() {
            return maximumCacheSize;
        }

        public void setMaximumCacheSize(int maximumCacheSize) {
            if (maximumCacheSize < 0) {
                throw new IllegalArgumentException("maximumCacheSize is negative");
            }
            this.maximumCacheSize = Math.max(minimumCacheSize, maximumCacheSize);
        }

        @Override
        protected boolean removeEldestEntry(Map.Entry<Tile, Image> eldest) {
            if (size() > maximumCacheSize) {
                eldest.getValue().flush();
                return true;
            } else {
                return false;
            }
        }

    }

    private final class MouseHandler extends MouseAdapter {

        private Point lastPoint;

        @Override
        public void mousePressed(MouseEvent e) {
            if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
                lastPoint = e.getPoint();
                setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
                lastPoint = null;
                setCursor(Cursor.getDefaultCursor());
                repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (lastPoint != null && (e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
                Point currentPoint = e.getPoint();
                scaledCenterX = scaledCenterX + lastPoint.x - currentPoint.x;
                scaledCenterY = scaledCenterY + lastPoint.y - currentPoint.y;
                lastPoint = currentPoint;
                firePropertyChange("center", null, null); // todo
                repaint();
            }
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            double centerX = scaledCenterX / scale;
            double centerY = scaledCenterY / scale;
            scale = Math.scalb(scale, e.getWheelRotation());
            // todo check scale bounds
            scaledCenterX = Math.round(centerX * scale);
            scaledCenterY = Math.round(centerY * scale);
            firePropertyChange("scale", null, null); // todo
            repaint();
        }

    }

    private final class TileHandler implements TileListener {

        @Override
        public void tileIterationsCalculated(final Tile tile, final int[] iterations) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    int width = tile.getWidth();
                    int height = tile.getHeight();
                    if (width == TILE_SIZE && height == TILE_SIZE && tile.getScale() == scale && tile.getFractal().equals(fractal)) {
                        int maxIteration = tile.getFractal().getMaxIteration();
                        BufferedImage image = (BufferedImage) tileImages.get(tile);
                        if (image == null) {
                            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                            tileImages.put(tile, image);
                        }
                        for (int y = 0; y < height; y++) {
                            for (int x = 0; x < width; x++) {
                                int iteration = iterations[y * width + x];
                                if (iteration == maxIteration) {
                                    image.setRGB(x, y, 0xff000000);
                                } else {
                                    image.setRGB(x, y, Color.HSBtoRGB(2.0f / 3.0f + (float) iteration / maxIteration, 1.0f, 1.0f) | 0xff000000);
                                }
                            }
                        }
                        repaint((int) (tile.getX() - scaledCenterX + getWidth() / 2), (int) (tile.getY() - scaledCenterY + getHeight() / 2), width, height);
                    }
                }
            });
        }

    }

}
